package com.example.hik.rest;

import com.alibaba.fastjson.JSONObject;
import com.example.hik.config.HikProperties;
import com.example.hik.rest.dto.TokenDTO;
import com.example.hik.rest.vm.HikRequest;
import com.example.hik.rest.vm.LyzxRequest;
import com.hikvision.artemis.sdk.ArtemisHttpUtil;
import com.hikvision.artemis.sdk.config.ArtemisConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.*;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import javax.net.ssl.*;
import java.io.*;
import java.net.*;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

/**
 * 接口代码
 *
 * @author maxwell
 * @since 2020-07-01
 * @since 2021-08-06 增加调用萤石云的通用接口 by maxwell
 */
@RestController
@RequestMapping("/api")
public class HikController {
    private final static Logger logger = LoggerFactory.getLogger(HikController.class);

    /**
     * 人员工种接口前缀
     */
    private static final String CCMS = "/ccms/";
    /**
     * 萤石云地址
     */
    private static final String YSURL = "https://open.ys7.com";
    /**
     * 浙江省流域中心视频 token 获取地址
     */
    private static final String LYZX_URL = "http://103.219.33.170:8081/QT/queryLiveToken.do";

    private static final Map<String, String> LYZX_MAP = new HashMap<>();

    private static Map<String, TokenDTO> ysTokenMap = new HashMap<>();
    private static Map<String, String> lyzxMap = new HashMap<>();

    private final HikProperties hikProperties;
    private final RestTemplate restTemplate;

    public HikController(HikProperties hikProperties) {
        this.hikProperties = hikProperties;
        this.restTemplate = new RestTemplate();

        LYZX_MAP.put("124.160.167.211:10000", "https://211.lives9.top:30443/api/v1/stream/start");
        LYZX_MAP.put("103.219.33.170:10000", "https://170.lives9.top:30443/api/v1/stream/start");
        LYZX_MAP.put("60.12.107.70:8224", "https://70.lives9.top:30443/api/v1/stream/start");
    }

    /**
     * 海康监控平台调用代理
     *
     * @param hikRequest 请求参数
     */
    @PostMapping("/hik")
    public ResponseEntity<String> hikRequest(@RequestBody HikRequest hikRequest)
            throws IOException, NoSuchAlgorithmException, KeyManagementException {
        if (StringUtils.isEmpty(hikRequest.getIp()) || StringUtils.isEmpty(hikRequest.getApiUrl())) {
            throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "ip和api地址不能为空。");
        }

        String appKey = hikRequest.getAppKey();
        String appSecret = hikRequest.getAppSecret();
        if (StringUtils.isEmpty(appKey)) {
            HikProperties.VideoKey info = hikProperties.getKeys().stream()
                    .filter(x -> x.getIp().equals(hikRequest.getIp()))
                    .findFirst()
                    .orElseThrow(() -> new HttpClientErrorException(HttpStatus.BAD_REQUEST, "无效的ip"));
            appKey = info.getAppKey();
            appSecret = info.getAppSecret();
        }

        ArtemisConfig.host = hikRequest.getIp();
        ArtemisConfig.appKey = appKey;
        ArtemisConfig.appSecret = appSecret;

        String apiUrl = hikRequest.getApiUrl();
        // 取人员相关数据，与视频平台接口分开
        if (apiUrl.contains(CCMS)) {
            return getCCMSRequest(hikRequest, apiUrl);
        }

        Map<String, String> path = new HashMap<>(1);
        String key = "https://";
        path.put(key, MessageFormat.format("/artemis{0}", apiUrl));

        String result = ArtemisHttpUtil.doPostStringArtemis(path, hikRequest.getApiParams(), null, null, "application/json");
        logger.debug("请求地址: {}，返回: {}", path.get(key), result);

        return ResponseEntity.ok()
                .header("content-type", "application/json")
                .body(result);
    }

    /**
     * 海康萤石云调用接口
     * https://open.ys7.com/doc/zh/book/index/address_v2.html
     *
     * @param hikRequest 请求参数
     */
    @PostMapping("/ys")
    public ResponseEntity<String> ysRequest(@RequestBody HikRequest hikRequest) {
        logger.info(hikRequest.toString());
        // 萤石云调用时，前端必须转入 appKey， secret可以传入也可以不传。如果不传入则使用后台配置的对应appKey的secret。
        String appKey = hikRequest.getAppKey();
        String appSecret = hikRequest.getAppSecret();
        if (StringUtils.isEmpty(appSecret)) {
            HikProperties.YsConfig info = hikProperties.getYs().stream()
                    .filter(x -> x.getAppKey().equals(appKey))
                    .findFirst()
                    .orElseThrow(() -> new HttpClientErrorException(HttpStatus.BAD_REQUEST, "无效的 appKey"));
            appSecret = info.getAppSecret();
        }

        String accessToken = getYsAccessToken(appKey, appSecret);
        String url = MessageFormat.format("{0}{1}?accessToken={2}&{3}",
                YSURL, hikRequest.getApiUrl(), accessToken, hikRequest.getApiParams());
        logger.info(MessageFormat.format("请求地址: {0}, token: {1}", url, accessToken));
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf("application/x-www-form-urlencoded"));
        HttpEntity<String> strEntity = new HttpEntity<>(null, headers);
        String result = restTemplate.postForObject(url, strEntity, String.class);
        // 如果是新版取流地址
        if (hikRequest.getApiUrl().equals("/api/lapp/v2/live/address/get")) {
            JSONObject obj = (JSONObject) JSONObject.parse(result);
            // 返回结果中添加 token
            if (obj != null && obj.containsKey("data")) {
                JSONObject data = obj.getJSONObject("data");
                data.put("accessToken", accessToken);
            }

            return ResponseEntity.ok()
                    .header("content-type", "application/json")
                    .body(JSONObject.toJSONString(obj));
        }
        return ResponseEntity.ok()
                .header("content-type", "application/json")
                .body(result);
    }

    /**
     * 流域中心视频取流地址
     *
     * @return 视频流信息
     */
    @PostMapping("/lyzx")
    public ResponseEntity<String> lyzxRequest(@RequestBody LyzxRequest request) {
        String serial = request.getSerial();
        String code = request.getCode();
        String address = request.getAddress();
        logger.info("流域中心视频取流: serial: {}, code: {}", serial, code);

        String token = getLyzxToken(address, false);
        String url = getLyzxUrl(address, serial, code, token);
        String result = getLyzxResponse(url);
        if (result.contains("Unauthorized")) {
            token = getLyzxToken(address, true);
            url = getLyzxUrl(address, serial, code, token);
            result = getLyzxResponse(url);
        }

        return ResponseEntity.ok()
                .header("content-type", "application/json")
                .body(result);
    }

    private String getLyzxUrl(String address, String serial, String code, String token) {
        return MessageFormat.format("{0}?serial={1}&code={2}&token={3}",
                LYZX_MAP.get(address), serial, code, token);
    }

    private String getLyzxResponse(String url) {
        logger.info(url);
        try {
            return restTemplate.getForObject(url, String.class);
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
            return ex.getMessage();
        }
    }

    /**
     * 得到萤石云 access token
     */
    private String getYsAccessToken(String appKey, String appSecret) {
        logger.info("取萤石云 AccessToken");

        TokenDTO tokenDTO = ysTokenMap.get(appKey);
        if (tokenDTO != null) {
            Instant expire = Instant.ofEpochMilli(tokenDTO.getExpireTime());
            if (Instant.now().isBefore(expire)) {
                return tokenDTO.getAccessToken();
            }
        }

        // https://open.ys7.com/doc/zh/book/index/user.html
        String url = MessageFormat.format("{0}/api/lapp/token/get?appKey={1}&appSecret={2}", YSURL, appKey, appSecret);
        logger.info(url);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf("application/x-www-form-urlencoded"));
        HttpEntity<String> strEntity = new HttpEntity<>(null, headers);
        String res = restTemplate.postForObject(url, strEntity, String.class);
        String token;
        JSONObject obj = (JSONObject) JSONObject.parse(res);
        if (obj != null && obj.getString("code").equals(String.valueOf(HttpStatus.OK.value()))) {
            JSONObject data = obj.getJSONObject("data");
            token = data.getString("accessToken");
            long expireTime = data.getLong("expireTime");
            ysTokenMap.put(appKey, new TokenDTO(token, expireTime));
        } else {
            throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "读取 AccessToken 失败。");
        }

        return token;
    }

    /**
     * 浙江省流域中心token
     */
    private String getLyzxToken(String address, boolean force) {
        logger.info("浙江省流域中心取 token");

        if (!force) {
            String token = lyzxMap.get(address);
            if (!StringUtils.isEmpty(token)) {
                return token;
            }
        }
        HttpHeaders headers = new HttpHeaders();
        HttpEntity<String> entity = new HttpEntity<>(null, headers);
        String res = restTemplate.postForObject(LYZX_URL, entity, String.class);
        JSONObject obj = (JSONObject) JSONObject.parse(res);
        if (obj != null && obj.getString("code").equals("0")) {
            JSONObject data = obj.getJSONObject("data");
            String token = data.getString(address);
            if (StringUtils.isEmpty(token)) {
                throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "地址: " + address + ", 不存在");
            }
            lyzxMap.put(address, token);
            return token;
        } else {
            throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "读取流域中心视频 Token 失败。");
        }
    }

    private ResponseEntity<String> getCCMSRequest(@RequestBody HikRequest hikRequest, String apiUrl) throws NoSuchAlgorithmException, KeyManagementException, IOException {
        logger.debug("收到 ccms 请求: {}", apiUrl);
        /* Start of Fix */
        TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }

            @Override
            public void checkClientTrusted(X509Certificate[] certs, String authType) {
            }

            @Override
            public void checkServerTrusted(X509Certificate[] certs, String authType) {
            }

        }};

        SSLContext sc = SSLContext.getInstance("SSL");
        sc.init(null, trustAllCerts, new java.security.SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

        // Create all-trusting host name verifier
        HostnameVerifier allHostsValid = new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        };
        // Install the all-trusting host verifier
        HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);
        URL url = new URL(MessageFormat.format("https://{0}{1}", hikRequest.getIp(), apiUrl));
        URLConnection conn = url.openConnection();
        InputStream inputstream = conn.getInputStream();
        InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
        BufferedReader bufferedreader = new BufferedReader(inputstreamreader);
        StringBuilder sb = new StringBuilder(1024);
        String string = null;
        while ((string = bufferedreader.readLine()) != null) {
            sb.append(string);
        }

        String resBody = sb.toString();
        if (StringUtils.isEmpty(resBody)) {
            logger.warn("请求的返回结果为空。");
            return ResponseEntity.ok("");
        }
        logger.debug(resBody);
        return ResponseEntity.ok()
                .header("content-type", "application/json")
                .body(resBody);
    }
}
